/*
	Copyright (C) 2011 Salil Bhagurkar

	This file is part of illusion

	illusion is free software: you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as
	published by the Free Software Foundation, either version 3 of
	the License, or (at your option) any later version.

	illusion is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public
	License along with illusion. 
	If not, see <http://www.gnu.org/licenses/>.
*/


/*(Table 00234)
Values for disk operation status:
 00h	successful completion
 01h	invalid function in AH or invalid parameter
 02h	address mark not found
 03h	disk write-protected
 04h	sector not found/read error
 05h	reset failed (hard disk)
 05h	data did not verify correctly (TI Professional PC)
 06h	disk changed (floppy)
 07h	drive parameter activity failed (hard disk)
 08h	DMA overrun
 09h	data boundary error (attempted DMA across 64K boundary or >80h sectors)
 0Ah	bad sector detected (hard disk)
 0Bh	bad track detected (hard disk)
 0Ch	unsupported track or invalid media
 0Dh	invalid number of sectors on format (PS/2 hard disk)
 0Eh	control data address mark detected (hard disk)
 0Fh	DMA arbitration level out of range (hard disk)
 10h	uncorrectable CRC or ECC error on read
 11h	data ECC corrected (hard disk)
 20h	controller failure
 31h	no media in drive (IBM/MS INT 13 extensions)
 32h	incorrect drive type stored in CMOS (Compaq)
 40h	seek failed
 80h	timeout (not ready)
 AAh	drive not ready (hard disk)
 B0h	volume not locked in drive (INT 13 extensions)
 B1h	volume locked in drive (INT 13 extensions)
 B2h	volume not removable (INT 13 extensions)
 B3h	volume in use (INT 13 extensions)
 B4h	lock count exceeded (INT 13 extensions)
 B5h	valid eject request failed (INT 13 extensions)
 B6h	volume present but read protected (INT 13 extensions)
 BBh	undefined error (hard disk)
 CCh	write fault (hard disk)
 E0h	status register error (hard disk)
 FFh	sense operation failed (hard disk)
*/

#include <arch/arch.h>
#include <klib/lib.h>
#include <klib/memory.h>
#include <kernel/errors.h>
#include <kernel/storage.h>
#include <kernel/console.h>
#include <kernel/types.h>

/*
 * Crude biosdisk driver.
 */

#define BD_INT 0x13
#define SECT_SZ 512

#define high(reg) ((u8)( reg >> 8 ))

static int reset_disk(u8 drive)
{
	struct bioscall_regs regs={0};
	regs.edx=drive;
	regs.f=BD_INT;
	bios_call(&regs);
	if(regs.f & 1)	{
		return high(((u16)regs.eax));
	}
	return 0;
}

static int reset_disk_retry(u8 drive,int retries)
{
	while(--retries>0)	{
		if((reset_disk(drive))==0)
			return 0;
	}
	return EFAIL;
}

#define DRIVE 0x80

struct drive_struct	{
	int cylinders;
	int sectors;
	int heads;
	u32 total_sect;
};

static struct drive_struct drive,myparameters;

static int get_drive_parameters()
{
	int cl;
	struct bioscall_regs regs={0};
	reset_disk_retry(0x80,3);
	regs.eax=0x800;
	regs.edx=DRIVE;
	regs.f=BD_INT;
	bios_call(&regs);
	if(regs.f & 1)	{
		return EFAIL;
	}
	cl=(u8)regs.ecx;
	drive.cylinders=high(((u16)regs.ecx));
	drive.cylinders|=((cl & 0xc0)<<2);
	drive.sectors=cl & 0x3f;
	drive.heads=high(((u16)regs.edx))+1;
	return 0;
}

static int get_chs(u32 LBA)
{
	if(drive.sectors==0)	{
		return ESTINV;
	}
	myparameters.sectors=(LBA%(drive.sectors))+1;
	myparameters.cylinders=(LBA/(drive.sectors))/(drive.heads);
	myparameters.heads=(LBA/(drive.sectors))%(drive.heads);
	return 0;
}

static int read_prim(void *buf,u32 LBA,u32 n)
{
	u8 ch,cl;
	u16 c;
	u16 nr;
	struct bioscall_regs regs={0};
	get_chs(LBA);
	ch=(u8)(myparameters.cylinders);
	cl=myparameters.sectors;
	cl|=(((myparameters.cylinders)&768)>>2);
	c=ch*0x100+cl;
	nr=(u16)n;
	regs.eax=0x0200+nr;
	regs.ebx=0x1000;
	regs.ecx=c;
	regs.edx=(myparameters.heads)*0x100+0x80;
	regs.f=BD_INT;
	bios_call(&regs);
	if(regs.f & 1)	{
		return high((u16)regs.eax);
	}
	memcpy(buf,(void *)0x1000,n*0x200);
	return 0;
}

static int write_prim(void *buf, u32 LBA, u32 n)
{
	u8 ch,cl;
	u16 c;
	u16 nr;
	struct bioscall_regs regs={0};
	get_chs(LBA);
	ch=(u8)(myparameters.cylinders);
	cl=myparameters.sectors;
	cl|=(((myparameters.cylinders)&768)>>2);
	c=ch*0x100+cl;
	nr=(u16)n;
	regs.eax=0x0300+nr;
	regs.ebx=0x1000;
	regs.ecx=c;
	regs.edx=(myparameters.heads)*0x100+0x80;
	regs.f=BD_INT;
	memcpy((void *)0x1000,buf,n*0x200);
	bios_call(&regs);
	if(regs.f & 1)	{
		return high((u16)regs.eax);
	}
	return 0;
}

//TODO Relocate dap_struct and drive_parms to a safe location,
// or fetch from memory manager.

struct dap_struct	{
	u8 size;
	u8 rsvd;
	u16 n;
	u32 buf;
	u64 start;
}*dap=(struct dap_struct *)0x9000;

struct drive_parms {
	u16 sz;
	u16 info;
	u32 cyl;
	u32 head;
	u32 sect;
	u64 total_sect;
	u8 pad[200];
}__attribute__((packed));
static struct drive_parms *drive_parms=(struct drive_parms *)0x9100;

static int check_ext()
{
	struct bioscall_regs regs={0};
	regs.eax=0x4100;
	regs.ebx=0x55aa;
	regs.ecx=0;
	regs.edx=0x80;
	regs.f=BD_INT;
	bios_call(&regs);
	if(regs.ebx==0xaa55)	{
	} else	{
		return ENOSUP;
	}
	regs.eax=0x4800;
	regs.ebx=0;
	regs.edx=0x80;
	regs.esi=(u32)drive_parms;
	regs.f=BD_INT;
	bios_call(&regs);
	if(regs.f & 1) {
		return EFAIL;
	}
	return 0;
}

static int read_drive(void *buf,u32 LBA,u32 n)
{
	struct bioscall_regs regs={0};
	if(!buf)
		return EPARAM;
	if(LBA>drive_parms->total_sect)
		return EPARAM;
	dap->n=(u16)n;
	dap->buf=0x1000;
	dap->start=LBA;
	regs.eax=0x4200;
	regs.ebx=0;
	regs.ecx=0;
	regs.edx=0x80;
	regs.esi=(u32)dap;
	regs.edi=0;
	regs.f=BD_INT;
	bios_call(&regs);
	memcpy(buf,(u8 *)0x1000,(dap->n)*512);
	if(regs.f & 1) {
		return (u16)regs.eax;
	} else {
		return 0;
	}
}

static int write_drive(void *buf,u32 LBA,u32 n)
{
	struct bioscall_regs regs={0};
		return ENOSUP;
	if(!buf)
		return EPARAM;
	dap->n=(u16)n;
	dap->buf=0x1000;
	dap->start=LBA;
	regs.eax=0x4300;
	regs.ebx=0;
	regs.ecx=0;
	regs.edx=0x80;
	regs.esi=(u32)dap;
	regs.edi=0;
	regs.f=BD_INT;
	bios_call(&regs);
	memcpy(buf,(u8 *)0x1000,dap->n*512);
	if(regs.f & 1)
		return (u16)regs.eax;
	else
		return 0;
}

static struct storage_dev hd0_dev = {.name = "hda"};



static int init_hdbiosdisk()
{
	u8 buf[512];
	reset_disk(DRIVE);
	hd0_dev.block_size = 512;
	hd0_dev.next = hd0_dev.prev = null;
	if(!check_ext()) {
		debug("hda : %u blocks\n",drive_parms->total_sect);
		drive.total_sect = hd0_dev.total_blocks = drive_parms->total_sect;
		hd0_dev.read = read_drive;
		hd0_dev.write = write_drive;
	} else {
		debug("hda: Using legacy read\n");
		get_drive_parameters();
		debug(" chs : %u,%u,%u\n",drive.cylinders,drive.heads,drive.sectors);
		drive.total_sect = hd0_dev.total_blocks = drive.cylinders * drive.heads * drive.sectors;
		hd0_dev.read = read_prim;
		hd0_dev.write = write_prim;
	}
	register_storage_dev(&hd0_dev);
	return 0;
}



static struct drive_struct fdparams;

static int get_chs_fd(u32 LBA)
{
	fdparams.sectors=(LBA%(18))+1;
	fdparams.cylinders=(LBA/(18))/(2);
	fdparams.heads=(LBA/(18))%(2);
	return 0;
}

static int __read_fd(void *buf, u32 LBA, u32 n)
{
	u8 ch,cl;
	u16 c;
	u16 nr;
	struct bioscall_regs regs={0};
	get_chs_fd(LBA);
	ch=(u8)(fdparams.cylinders);
	cl=fdparams.sectors;
	cl|=(((fdparams.cylinders)&768)>>2);
	c=ch*0x100+cl;
	nr=(u16)n;
	regs.eax=0x0200+nr;
	regs.ebx=0x1000;
	regs.ecx=c;
	regs.edx=(fdparams.heads)*0x100;
	regs.f=BD_INT;
	bios_call(&regs);
	if(regs.f & 1)	{
		return high((u16)regs.eax);
	}
	memcpy(buf,(void *)0x1000,n*0x200);
	return 0;
}

static int read_fd(void *buf,u32 LBA,u32 n)
{
	void *buf_i = buf;
	while(n-- > 0) {
		int err = __read_fd(buf_i, LBA, 1);
		if(err) {
			return err;
		}
		buf_i += 512;
		LBA++;
	}
	return 0;
}

static int __write_fd(void *buf,u32 LBA,u32 n)
{
	u8 ch,cl;
	u16 c;
	u16 nr;
	struct bioscall_regs regs={0};
	get_chs_fd(LBA);
	ch=(u8)(fdparams.cylinders);
	cl=fdparams.sectors;
	cl|=(((fdparams.cylinders)&768)>>2);
	c=ch*0x100+cl;
	nr=(u16)n;
	regs.eax=0x0300+nr;
	regs.ebx=0x1000;
	regs.ecx=c;
	regs.edx=(fdparams.heads)*0x100;
	regs.f=BD_INT;
	memcpy((void *)0x1000, buf, n*0x200);
	bios_call(&regs);
	if(regs.f & 1)	{
		return high((u16)regs.eax);
	}
	return 0;
}

static int write_fd(void *buf,u32 LBA,u32 n)
{
	void *buf_i = buf;
	while(n-- > 0) {
		int err = __write_fd(buf_i, LBA, 1);
		if(err)
			return err;
		buf_i += 512;
		LBA--;
	}
	return 0;
}

struct storage_dev fd_dev = {.name = "fda"};

static int init_fdbiosdisk()
{
	u8 buf[512];
	if(reset_disk_retry(0, 3))
		return EFAIL;

	debug("Floppy drive detected: 2880 blocks\n");
	fd_dev.block_size = 512;
	fd_dev.total_blocks = 2880; //:( Not much support
	fd_dev.next = fd_dev.prev = null;
	fd_dev.read = read_fd;
	fd_dev.write = write_fd;

	register_storage_dev(&fd_dev);

	return 0;

}


int init_biosdisk()
{
	//Detect the fd
	init_fdbiosdisk();
	//Detect the HD
	init_hdbiosdisk();

	return 0;
}
